namespace System.Xml.Serialization
{
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using System.Xml.Linq;
    using Internal;

    public sealed class XmlObjectSerializer :
        IXmlObjectSerializer,
        IXmlRuntimeSerializer,
        IXmlObjectAttributeSerializer,
        IXmlObjectAttributeReader,
        IXmlObjectAttributeWriter,
        IXmlAttributeWriter,
        IXmlRuntimeReader,
        IXmlRuntimeWriter,
        IXmlRuntimePropertyReader,
        IXmlRuntimePropertyWriter,
        IXmlPropertyReader,
        IXmlPropertyWriter
    {
        private int _count;
        private int _id;
        private XElement _element;
        private Dictionary<int, XmlObjectSerializer> _nodes;

        public IXmlObjectSerializer<T> GetSerializer<T>()
        {
            return new XmlObjectSerializer<T>(this);
        }
        private int GetCount(string propertyName)
        {
            IList<XmlObjectSerializer> collection = new List<XmlObjectSerializer>(_nodes.Values.TakeWhile((property) => property.Name == propertyName));
            if (collection.Count > 0)
            {
                return collection.Count;
            }
            return _nodes.Values.Count((p) => p.Name == propertyName);
        }
        IEnumerable<T> IXmlPropertyReader.Read<T>(string propertyName, Factory<T> constructor)
        {
            foreach (IXmlObjectSerializer serializer in _nodes.Values)
            {
                if (serializer.Name == propertyName)
                {
                    T t = constructor();
                    serializer.Deserialize(t);
                    yield return t;
                }
            }
        }
        string IXmlPropertyReader.Read(string propertyName)
        {
            IList<XmlObjectSerializer> collection = new List<XmlObjectSerializer>(_nodes.Values.TakeWhile((property) => property.Name == propertyName));
            if (collection.Count > 0)
            {
                return collection[0].Value;
            }
            XmlObjectSerializer node = GetObjectSerializer(propertyName);
            return node.Value;
        }
        void IXmlPropertyWriter.Write<T>(string propertyName, IEnumerable<T> objects)
        {
            foreach (T t in objects)
            {
                IXmlObjectSerializer serializer = CreateInternal(propertyName);
                serializer.Serialize(t);
            }
        }
        void IXmlPropertyWriter.Write(string propertyName, string value)
        {
            IList<XmlObjectSerializer> collection = new List<XmlObjectSerializer>(_nodes.Values.TakeWhile((property) => property.Name == propertyName));
            if (collection.Count > 0)
            {
                collection[0].Value = value;
                return;
            }
            XmlObjectSerializer node = GetObjectSerializer(propertyName);
            node.Value = value;
        }

        private int Create(XmlObjectSerializer child)
        {
            _children.Add(child);
            return ++_count;
        }

        IXmlObjectAttributeReader IXmlAttributeReader.ReadAttributes { get { return this; } }
        IXmlObjectAttributeWriter IXmlAttributeWriter.WriteAttributes { get { return this; } }
        string IXmlObjectAttributeReader.this[string attributeName] { get { return Attributes[attributeName]; } }
        string IXmlObjectAttributeWriter.this[string attributeName] { set { Attributes[attributeName] = value; } }

        private IXmlPropertyReader _propertyReader;
        private IXmlPropertyWriter _propertyWriter;

        private XmlObjectSerializer _parent;
        private List<XmlObjectSerializer> _children = new List<XmlObjectSerializer>();
        private string _name;

        private XmlObjectSerializer(XmlObjectSerializer parent, XElement element)
        {
            _parent = parent;
            _id = _parent != null ? _parent.Create(this) : 0;
            _element = element;
            _name = _element.Name.ToString();
            _propertyReader = this;
            _propertyWriter = this;
            _nodes = new Dictionary<int, XmlObjectSerializer>();
            _nodes.Add(0, this);
        }

        public int Id { get { return _id; } }
        public string Name { get { return _name; } }
        public IXmlObjectAttributeSerializer Attributes { get { return this; } }

        public IEnumerable<IXmlObjectSerializer> Elements
        {
            get
            {
                foreach (XmlObjectSerializer serializer in _nodes.Values)
                {
                    yield return serializer;
                }
            }
        }
        public string this[string propertyName]
        {
            get { return _propertyReader.Read(propertyName); }
            set { _propertyWriter.Write(propertyName, value); }
        }
        private string Value
        {
            get
            {
                return _element.Value;
            }
            set
            {
                _element.SetValue(value);
            }
        }

        private XElement Element { get { return _element; } }

        private string ToXmlString() { return _element.ToString(); }

        private XmlObjectSerializer LoadElement(XElement root)
        {
            return Load(root, CreateInternal(root.Name.ToString()));
        }

        private static XmlObjectSerializer Load(XElement root, XmlObjectSerializer serializer)
        {
            if (root.HasAttributes)
            {
                foreach (XAttribute attribute in root.Attributes())
                {
                    serializer.Attributes[attribute.Name.LocalName] = attribute.Value;
                }
            }
            if (root.HasElements)
            {
                foreach (XElement element in root.Elements())
                {
                    serializer.LoadElement(element);
                }
            }
            if (root.NodeType == XmlNodeType.Element)
            {
                serializer.Value = root.Value;
            }
            return serializer;
        }

        private void Add(XmlObjectSerializer serializer)
        {
            if (!_nodes.Keys.Contains(serializer.Id))
            {
                _element.Add(serializer.Element);
                _nodes.Add(serializer.Id, serializer);
            }
        }

        private void Remove(XmlObjectSerializer serializer)
        {
            if (_nodes.Keys.Contains(serializer.Id))
            {
                serializer.Element.Remove();
                _nodes.Remove(serializer.Id);
            }
        }

        private XmlObjectSerializer FindByName(string propertyName)
        {
            IList<XmlObjectSerializer> collection = new List<XmlObjectSerializer>(_nodes.Values.TakeWhile((property) => property.Name == propertyName));
            if (collection.Count > 0)
            {
                return collection[0];
            }
            return null;
        }

        public override string ToString()
        {
            return _name;
        }

        void IXmlRuntimeSerializer.Deserialize(Type type, object value)
        {
            IEnumerable<IXmlRuntimeProperty> properties = CreateProperties(type, value);
            foreach (IRuntimePropertyReader property in properties)
            {
                string propertyName;
                if (property.CanRuntimeSerialize)
                {
                    Type serializerType = property.RuntimeSerializer.GetType();
                    if (serializerType == typeof(AttributeRuntimeSerializer) ||
                        serializerType == typeof(ElementRuntimeSerializer))
                    {
                        IXmlPropertyReader propertyReader = this;
                        propertyName = property.PropertyName;
                        property.Writer.WriteObject(property.RuntimeSerializer.Deserialize(propertyReader, propertyName));
                        continue;
                    }
                    IXmlRuntimeReader reader = this;
                    propertyName = property.PropertyName;
                    reader.Read(property, GetObjectSerializer(propertyName), propertyName);
                    continue;
                }
                if (property.CanSerialize)
                {
                    IXmlRuntimePropertyReader reader = this;
                    IXmlPropertyReader propertyReader = this;
                    propertyName = property.PropertyName;
                    reader.Read(propertyReader, property.Writer, propertyName);
                    continue;
                }
            }
        }
        void IXmlRuntimeSerializer.Serialize(Type type, object value)
        {
            IEnumerable<IXmlRuntimeProperty> properties = CreateProperties(type, value);
            foreach (IRuntimePropertyWriter property in properties)
            {
                string propertyName;
                if (property.CanRuntimeSerialize)
                {
                    Type serializerType = property.RuntimeSerializer.GetType();
                    if (serializerType == typeof(AttributeRuntimeSerializer) ||
                        serializerType == typeof(ElementRuntimeSerializer))
                    {
                        IXmlPropertyWriter propertyWriter = this;
                        propertyName = property.PropertyName;
                        property.RuntimeSerializer.Serialize(property.Reader.ReadObject(), propertyWriter, propertyName);
                        continue;
                    }
                    IXmlRuntimeWriter writer = this;
                    propertyName = property.PropertyName;
                    writer.Write(property, GetObjectSerializer(propertyName), propertyName);
                    continue;
                }
                if (property.CanSerialize)
                {
                    IXmlRuntimePropertyWriter writer = this;
                    IXmlPropertyWriter propertyWriter = this;
                    propertyName = property.PropertyName;
                    writer.Write(propertyWriter, property.Reader, propertyName);
                    continue;
                }
            }
        }
        void IXmlRuntimeSerializer.Deserialize(object value)
        {
            IEnumerable<XmlRuntimeProperty> properties = CreatePropertiesInternal(value);
            foreach (XmlRuntimeProperty property in properties)
            {
                string propertyName;
                if (property.CanRuntimeSerialize)
                {
                    Type serializerType = property.RuntimeSerializer.GetType();
                    if (serializerType == typeof(AttributeRuntimeSerializer) ||
                        serializerType == typeof(ElementRuntimeSerializer))
                    {
                        IXmlPropertyReader propertyReader = this;
                        propertyName = property.PropertyName;
                        property.RuntimeSerializer.Deserialize(propertyReader, propertyName);
                        continue;
                    }
                    IXmlRuntimeReader reader = this;
                    propertyName = property.PropertyName;
                    reader.Read(property, GetObjectSerializer(propertyName), propertyName);
                    continue;
                }
                if (property.CanSerialize)
                {
                    IXmlRuntimePropertyReader reader = this;
                    IXmlPropertyReader propertyReader = this;
                    propertyName = property.PropertyName;
                    reader.Read(propertyReader, property, propertyName);
                    continue;
                }
            }
        }
        void IXmlRuntimeSerializer.Serialize(object value)
        {
            IEnumerable<XmlRuntimeProperty> properties = CreatePropertiesInternal(value);
            foreach (XmlRuntimeProperty property in properties)
            {
                string propertyName;
                if (property.CanRuntimeSerialize)
                {
                    Type serializerType = property.RuntimeSerializer.GetType();
                    if (serializerType == typeof(AttributeRuntimeSerializer) ||
                        serializerType == typeof(ElementRuntimeSerializer))
                    {
                        IXmlPropertyWriter propertyWriter = this;
                        propertyName = property.PropertyName;
                        property.RuntimeSerializer.Serialize(property.Reader.Read(), propertyWriter, propertyName);
                        continue;
                    }
                    IXmlRuntimeWriter writer = this;
                    propertyName = property.PropertyName;
                    writer.Write(property, GetObjectSerializer(propertyName), propertyName);
                    continue;
                }
                if (property.CanSerialize)
                {
                    IXmlRuntimePropertyWriter writer = this;
                    IXmlPropertyWriter propertyWriter = this;
                    propertyName = property.PropertyName;
                    writer.Write(propertyWriter, property, propertyName);
                    continue;
                }
            }
        }

        private static XmlObjectSerializer CreateInternal(XmlObjectSerializer parent, string name)
        {
            return new XmlObjectSerializer(parent, new XElement(XName.Get(name)));
        }

        private static string GetRootName(Type type, string elementName)
        {
            XmlRootSerializerAttribute[] attributes = XmlRuntimePropertyHelper.GetInterfaceAttributes<XmlRootSerializerAttribute>(type);
            if (attributes.Length > 0)
            {
                return attributes[0].Name;
            }
            return elementName;
        }

        private XmlObjectSerializer FindInternal(string propertyName)
        {
            return _nodes.Values.FirstOrDefault((p) => p.Name == propertyName);
        }

        public static IXmlObjectSerializer CreateSerializer(XElement root, string name)
        {
            return Load(root, CreateInternal(null, name));
        }

        public static IXmlObjectSerializer Load(string xml)
        {
            using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
            {
                XElement root = XElement.Load(XmlReader.Create(ms));
                return Load(root, CreateInternal(null, root.Name.ToString()));
            }
        }

        public static IXmlObjectSerializer Create(string name)
        {
            return CreateInternal(null, name);
        }

        public static IXmlObjectSerializer<T> Create<T>(string xml)
        {
            using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
            {
                XElement root = XElement.Load(XmlReader.Create(ms));
                string rootName = GetRootName(typeof(T), root.Name.ToString());
                XmlObjectSerializer serializer = Load(root, CreateInternal(null, rootName));
                return serializer.GetSerializer<T>();
            }
        }

        public static void Load<T>(string xml, Type type, object value)
        {
            using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
            {
                XDocument root = XDocument.Load(XmlReader.Create(ms));
                string rootName = GetRootName(typeof(T), root.Root.Name.ToString());
                IXmlObjectSerializer serializer = Load(root.Root, CreateInternal(null, rootName));
                serializer.Deserialize(type, value);
            }
        }
        public static void Load<T>(string xml, object value)
        {
            using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
            {
                XDocument root = XDocument.Load(XmlReader.Create(ms));
                string rootName = GetRootName(typeof(T), root.Root.Name.ToString());
                IXmlObjectSerializer serializer = Load(root.Root, CreateInternal(null, rootName));
                serializer.Deserialize(typeof(T), value);
            }
        }
        public static string Save<T>(Type type, object value)
        {
            string rootName = GetRootName(typeof(T), value.GetType().Name);
            XmlObjectSerializer result = CreateInternal(null, rootName);
            IXmlRuntimeSerializer serializer = result;
            serializer.Serialize(type, value);
            XDocument root = new XDocument();
            return result.ToXmlString();
        }
        public static string Save<T>(object value)
        {
            string rootName = GetRootName(typeof(T), value.GetType().Name);
            XmlObjectSerializer result = CreateInternal(null, rootName);
            IXmlRuntimeSerializer serializer = result;
            serializer.Serialize(typeof(T), value);
            return result.ToXmlString();
        }
        public static void Load(Stream data, Type type, object value)
        {
            XDocument document = XDocument.Load(XmlReader.Create(data));
            string rootName = GetRootName(type, document.Root.Name.ToString());
            IXmlObjectSerializer serializer = Load(document.Root, CreateInternal(null, rootName));
            serializer.Deserialize(type, value);
        }
        public static void Save(Stream data, Type type, object value)
        {
            string rootName = GetRootName(type, value.GetType().Name);
            XmlObjectSerializer serializer = CreateInternal(null, rootName);
            serializer.Save(type, data, value);
        }
        private void Save(Type type, Stream data, object value)
        {
            IXmlObjectSerializer serializer = this;
            serializer.Serialize(type, value);
            XDocument document = XDocument.Parse(_element.ToString());
            document.Save(XmlWriter.Create(data));
        }
        private XmlObjectSerializer GetObjectSerializer(string propertyName)
        {
            return FindInternal(propertyName) ?? CreateInternal(propertyName);
        }
        private XmlObjectSerializer CreateInternal(string propertyName)
        {
            XmlObjectSerializer serializer = CreateInternal(this, propertyName);
            _element.Add(serializer.Element);
            _nodes.Add(serializer.Id, serializer);
            return serializer;
        }

        private void Clear()
        {
            _children.Clear();
            _element.RemoveAll();
            _nodes.Clear();
        }

        string IXmlObjectAttributeSerializer.this[string attributeName]
        {
            get
            {
                return _element.Attribute(XName.Get(attributeName)).Value;
            }
            set
            {
                _element.SetAttributeValue(XName.Get(attributeName), value);
            }
        }

        #region IXmlObjectSerializer

        void IXmlObjectSerializer.Add(IXmlObjectSerializer serializer)
        {
            XmlObjectSerializer serializerInternal = serializer as XmlObjectSerializer;
            if (serializerInternal != null)
            {
                Add(serializerInternal);
            }
        }
        void IXmlObjectSerializer.Remove(IXmlObjectSerializer serializer)
        {
            XmlObjectSerializer serializerInternal = serializer as XmlObjectSerializer;
            if (serializerInternal != null)
            {
                Remove(serializerInternal);
            }
        }
        IXmlObjectSerializer IXmlObjectSerializer.Create(object value, string name)
        {
            XmlObjectSerializer node = FindByName(name) ?? CreateInternal(this, name);
            if (node != null)
            {
                IXmlRuntimeSerializer serializer = node;
                serializer.Serialize(value);
            }
            return node;
        }
        IXmlObjectSerializer IXmlObjectSerializer.Add(object value, string name)
        {
            XmlObjectSerializer node = FindByName(name);
            if (node != null)
            {
                IXmlRuntimeSerializer serializer = node;
                serializer.Serialize(value);
                Add(node);
            }
            return node;
        }
        IXmlObjectSerializer IXmlObjectSerializer.Remove(object value, string name)
        {
            XmlObjectSerializer node = FindByName(name);
            if (node != null)
            {
                IXmlRuntimeSerializer serializer = node;
                serializer.Deserialize(value);
                Remove(node);
            }
            return node;
        }

        #endregion

        #region IXmlReader Members

        void IXmlRuntimePropertyReader.Read(IXmlPropertyReader propertyReader, IRuntimePropertyWriter property, string propertyName)
        {
            property.Write(propertyReader.Read(propertyName));
        }
        void IXmlRuntimeReader.Read(IXmlReader reader, IXmlPropertyReader propertyReader, string propertyName)
        {
            reader.Read(propertyReader, propertyName);
        }

        #endregion

        #region IXmlWriter Members

        void IXmlRuntimePropertyWriter.Write(IXmlPropertyWriter propertyWriter, IRuntimePropertyReader property, string propertyName)
        {
            propertyWriter.Write(propertyName, property.Read());
        }
        void IXmlRuntimeWriter.Write(IXmlWriter writer, IXmlPropertyWriter propertyWriter, string propertyName)
        {
            writer.Write(propertyWriter, propertyName);
        }

        #endregion

        private static IEnumerable<XmlRuntimeProperty> CreatePropertiesInternal(object value)
        {
            Type type = value.GetType();
            List<XmlRuntimeProperty> controlProperties = new List<XmlRuntimeProperty>();
            foreach (PropertyInfo property in XmlRuntimePropertyHelper.GetInterfaceProperties(type))
            {
                Type reflectedType = property.ReflectedType;
                XmlRuntimeProperty controlProperty = CreateProperty(value, reflectedType, property);
                if (controlProperty != null)
                {
                    controlProperties.Add(controlProperty);
                }
            }
            return controlProperties;
        }

        private static IEnumerable<IXmlRuntimeProperty> CreateProperties(Type type, object value)
        {
            Type objectType = value.GetType();
            Type[] objectInterfaces = objectType.GetInterfaces();
            List<IXmlRuntimeProperty> controlProperties = new List<IXmlRuntimeProperty>();
            foreach (PropertyInfo property in XmlRuntimePropertyHelper.GetInterfacePropertiesWithType(type))
            {
                Type reflectedType = property.ReflectedType;
                if (objectInterfaces.Contains(reflectedType))
                {
                    XmlRuntimeProperty controlProperty = CreateProperty(value, reflectedType, property);
                    if (controlProperty != null)
                    {
                        controlProperties.Add(controlProperty);
                    }
                }
                else
                {
                    XmlRuntimeProperty controlProperty = CreateProperty(value, reflectedType, objectType.GetProperty(property.Name));
                    if (controlProperty != null)
                    {
                        controlProperties.Add(controlProperty);
                    }
                }
            }
            return controlProperties;
        }

        private static XmlRuntimeProperty CreateProperty(object value, string propertyName)
        {
            Type type = value.GetType();
            PropertyInfo property = XmlRuntimePropertyHelper.GetInterfaceProperty(type, propertyName);
            if (property != null)
            {
                return new XmlRuntimeProperty(value, type, property);
            }
            return null;
        }

        private static XmlRuntimeProperty CreateProperty(object value, Type type, PropertyInfo property)
        {
            if (property != null)
            {
                return new XmlRuntimeProperty(value, type, property);
            }
            return null;
        }
    }
}